Skip to content

agentling/feature/memory#3

Merged
folathecoder merged 3 commits into
mainfrom
agentling/feature/memory
Jul 6, 2026
Merged

agentling/feature/memory#3
folathecoder merged 3 commits into
mainfrom
agentling/feature/memory

Conversation

@folathecoder

Copy link
Copy Markdown
Owner
  • Add memory.py: typed steps, Memory, and JSON round-trip
  • Add events.py: frozen streaming event types (TextDelta, ToolCall/Result, Step, Final)
  • Add unit tests for memory.py (message shapes, error rendering, JSON round-trip)

Record each turn as a typed Step (TaskStep/ActionStep/FinalStep) that renders
itself to ChatMessages, rather than storing a flat message list. Steps carry
usage/timing/errors; failed tools render as recoverable observations. Memory
derives the model's message list via to_messages(system_prompt), and
dump_json/load_json round-trip the whole run (versioned) for persistence/replay.
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add typed run Memory with step records, streaming Events, and JSON persistence

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Introduce typed run memory steps (task/action/final) that render provider messages.
• Support JSON dump/load for persistence and replay, with versioned step tagging.
• Add frozen streaming event dataclasses and tests for rendering, errors, and round-trip.
Diagram

graph TD
A["Agent loop"] --> B["Memory"] --> C["Steps"]
B --> H[("JSON")]
A --> D["Model adapter"]
A --> E["Tools"]
A --> F["Events"] --> G["Caller"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a serialization library (pydantic / dataclasses-json)
  • ➕ Less custom (de)serialization code; automatic nested type handling
  • ➕ Easier schema evolution/validation (e.g., versioned migrations)
  • ➖ Adds a dependency and constrains runtime/typing choices
  • ➖ May still require explicit union discriminators for Step variants
2. Store raw ChatMessages plus parallel metadata
  • ➕ Avoids custom union tagging; minimal transformation between runtime and model I/O
  • ➕ Easier interop with provider adapters since messages are already canonical
  • ➖ Harder to associate tool results/errors/timing with the exact model turn reliably
  • ➖ More fragile persistence format (implicit coupling to provider message shapes)

Recommendation: The PR’s approach (typed Steps with explicit discriminators and to_messages()) is a good fit for durable replay and for attaching per-turn metadata (usage/timing/tool results) without polluting the provider-facing message list. Consider a serialization library only if you expect rapid schema evolution or need stricter validation across versions.

Files changed (3) +410 / -0

Enhancement (2) +258 / -0
events.pyIntroduce frozen streaming event types for agent run progress +56/-0

Introduce frozen streaming event types for agent run progress

• Adds immutable dataclass event types for streamed text, tool call lifecycle, step recording, and final completion. Exposes a public Event union for the values yielded by an agent event stream.

src/agentling/events.py

memory.pyAdd typed Step records and Memory with JSON round-trip +202/-0

Add typed Step records and Memory with JSON round-trip

• Implements TaskStep/ActionStep/FinalStep with per-step rendering back to ChatMessage sequences, including recoverable error observations for failed tool calls. Adds Memory to store ordered steps, render full conversations with an injected system prompt, and serialize/deserialize a versioned JSON format with step-type tagging.

src/agentling/memory.py

Tests (1) +152 / -0
test_memory.pyAdd unit tests for step rendering, Memory behavior, and JSON persistence +152/-0

Add unit tests for step rendering, Memory behavior, and JSON persistence

• Covers message shapes for each step type, Memory.to_messages behavior, add/reset multi-turn support, and JSON round-trip correctness. Includes negative testing for unknown serialized step kinds and verifies error-flag preservation.

tests/test_memory.py

@folathecoder folathecoder merged commit f99a04d into main Jul 6, 2026
2 checks passed
@folathecoder folathecoder deleted the agentling/feature/memory branch July 6, 2026 17:56
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Fragile JSON load path 🐞 Bug ☼ Reliability
Description
Memory.from_dict() iterates over data.get('steps', []) without normalizing/validating, so inputs
like {"steps": null} raise TypeError and malformed entries raise KeyError deep in helpers instead of
a clear ValueError. The serializer emits a version field but the loader ignores it, making format
evolution harder and failures less diagnosable.
Code

src/agentling/memory.py[R152-167]

+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> Memory:
+        """Rebuild a Memory from the dict produced by to_dict()."""
+
+        return cls(steps=[_step_from_dict(entry) for entry in data.get("steps", [])])
+
+    def dump_json(self) -> str:
+        """Serialize the memory to a JSON string."""
+
+        return json.dumps(self.to_dict())
+
+    @classmethod
+    def load_json(cls, raw: str) -> Memory:
+        """Rebuild a Memory from a JSON string produced by dump_json()."""
+
+        return cls.from_dict(json.loads(raw))
Evidence
The loader directly indexes required keys and does not validate the top-level structure or schema
version. This makes deserialization fail with KeyError/TypeError on inputs that deviate slightly
from the exact asdict() shape (including steps: null), and the written version field is
currently unused.

src/agentling/memory.py[137-167]
src/agentling/memory.py[172-200]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`Memory.from_dict()` / `_step_from_dict()` / `_chat_message_from_dict()` assume a perfectly-shaped dict matching `to_dict()` output. If `steps` is present but `null` (or not a list), or if nested keys are missing (e.g. `tool_calls`, `tool_call_id`, `tool_results`, `error`, `duration`), loading raises low-level `TypeError`/`KeyError` rather than a controlled `ValueError`. Also, `to_dict()` writes a `version` field but `from_dict()` never checks it.

### Issue Context
This code is intended for persistence/replay. In practice, stored JSON can be manually edited, partially migrated, or come from a newer/older schema version.

### Fix Focus Areas
- src/agentling/memory.py[152-168]
- src/agentling/memory.py[172-200]

### What to change
- In `from_dict`, normalize `steps = data.get("steps") or []` and validate it’s a list of dict entries; raise `ValueError` with a helpful message if invalid.
- Read and validate `version` (e.g. default to 1; raise on unsupported versions).
- In `_chat_message_from_dict` and `_step_from_dict`, use `.get()` with sensible defaults for optional fields (`tool_calls`, `tool_call_id`, `usage`, `tool_results`, `error`, `duration`) and raise `ValueError` on missing required fields (`role`, `content`, etc.) rather than allowing `KeyError`.
- Optionally, accept missing optional keys for backward compatibility (e.g. treat missing `tool_calls` as `[]`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Subclass steps break serialization 🐞 Bug ⚙ Maintainability
Description
Memory.to_dict() uses self._KIND[type(step)] for tagging, so any subclass of
TaskStep/ActionStep/FinalStep will raise KeyError and prevent persistence. This makes the step
system unexpectedly non-extensible at runtime.
Code

src/agentling/memory.py[R144-149]

+        return {
+            "version": 1,
+            "steps": [
+                {"type": self._KIND[type(step)], "data": asdict(step)}
+                for step in self.steps
+            ],
Evidence
The serializer indexes the discriminator map with type(step) (exact runtime type), which will not
match any mapping keys for subclasses and will throw KeyError during serialization.

src/agentling/memory.py[105-150]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`Memory.to_dict()` tags steps via `self._KIND[type(step)]`, which only works for exact built-in step classes. Subclasses (even compatible ones) won’t be found and will crash serialization with `KeyError`.

### Issue Context
Even if the library doesn’t officially support custom step subclasses today, this behavior is surprising and easy to trip over when users add metadata via subclassing.

### Fix Focus Areas
- src/agentling/memory.py[105-150]

### What to change
- Replace exact-type lookup with a more robust discriminator:
 - Add a `kind: ClassVar[str]` on each step class, or
 - Resolve kind via `next(tag for cls, tag in _KIND.items() if isinstance(step, cls))`, raising a clear `ValueError` if no match.
- Consider adding a small unit test that verifies subclass instances either serialize correctly or fail with a clear error message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/agentling/memory.py
Comment on lines +152 to +167
@classmethod
def from_dict(cls, data: dict[str, Any]) -> Memory:
"""Rebuild a Memory from the dict produced by to_dict()."""

return cls(steps=[_step_from_dict(entry) for entry in data.get("steps", [])])

def dump_json(self) -> str:
"""Serialize the memory to a JSON string."""

return json.dumps(self.to_dict())

@classmethod
def load_json(cls, raw: str) -> Memory:
"""Rebuild a Memory from a JSON string produced by dump_json()."""

return cls.from_dict(json.loads(raw))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Fragile json load path 🐞 Bug ☼ Reliability

Memory.from_dict() iterates over data.get('steps', []) without normalizing/validating, so inputs
like {"steps": null} raise TypeError and malformed entries raise KeyError deep in helpers instead of
a clear ValueError. The serializer emits a version field but the loader ignores it, making format
evolution harder and failures less diagnosable.
Agent Prompt
### Issue description
`Memory.from_dict()` / `_step_from_dict()` / `_chat_message_from_dict()` assume a perfectly-shaped dict matching `to_dict()` output. If `steps` is present but `null` (or not a list), or if nested keys are missing (e.g. `tool_calls`, `tool_call_id`, `tool_results`, `error`, `duration`), loading raises low-level `TypeError`/`KeyError` rather than a controlled `ValueError`. Also, `to_dict()` writes a `version` field but `from_dict()` never checks it.

### Issue Context
This code is intended for persistence/replay. In practice, stored JSON can be manually edited, partially migrated, or come from a newer/older schema version.

### Fix Focus Areas
- src/agentling/memory.py[152-168]
- src/agentling/memory.py[172-200]

### What to change
- In `from_dict`, normalize `steps = data.get("steps") or []` and validate it’s a list of dict entries; raise `ValueError` with a helpful message if invalid.
- Read and validate `version` (e.g. default to 1; raise on unsupported versions).
- In `_chat_message_from_dict` and `_step_from_dict`, use `.get()` with sensible defaults for optional fields (`tool_calls`, `tool_call_id`, `usage`, `tool_results`, `error`, `duration`) and raise `ValueError` on missing required fields (`role`, `content`, etc.) rather than allowing `KeyError`.
- Optionally, accept missing optional keys for backward compatibility (e.g. treat missing `tool_calls` as `[]`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/agentling/memory.py
Comment on lines +144 to +149
return {
"version": 1,
"steps": [
{"type": self._KIND[type(step)], "data": asdict(step)}
for step in self.steps
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Subclass steps break serialization 🐞 Bug ⚙ Maintainability

Memory.to_dict() uses self._KIND[type(step)] for tagging, so any subclass of
TaskStep/ActionStep/FinalStep will raise KeyError and prevent persistence. This makes the step
system unexpectedly non-extensible at runtime.
Agent Prompt
### Issue description
`Memory.to_dict()` tags steps via `self._KIND[type(step)]`, which only works for exact built-in step classes. Subclasses (even compatible ones) won’t be found and will crash serialization with `KeyError`.

### Issue Context
Even if the library doesn’t officially support custom step subclasses today, this behavior is surprising and easy to trip over when users add metadata via subclassing.

### Fix Focus Areas
- src/agentling/memory.py[105-150]

### What to change
- Replace exact-type lookup with a more robust discriminator:
  - Add a `kind: ClassVar[str]` on each step class, or
  - Resolve kind via `next(tag for cls, tag in _KIND.items() if isinstance(step, cls))`, raising a clear `ValueError` if no match.
- Consider adding a small unit test that verifies subclass instances either serialize correctly or fail with a clear error message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant